package edu.kit.aifb.ldstream.stat.util;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

import dk.ange.octave.OctaveEngine;
import dk.ange.octave.OctaveEngineFactory;
import dk.ange.octave.exception.OctaveException;
import dk.ange.octave.type.OctaveDouble;

import edu.kit.aifb.ldstream.stat.distrib.BinomialDistribution;
import edu.kit.aifb.ldstream.stat.distrib.ContinuousUniformDistribution;
import edu.kit.aifb.ldstream.stat.distrib.DiscreteUniformDistribution;
import edu.kit.aifb.ldstream.stat.distrib.ExponentialDistribution;
import edu.kit.aifb.ldstream.stat.distrib.IDistribution;
import edu.kit.aifb.ldstream.stat.distrib.NormalDistribution;
import edu.kit.aifb.ldstream.stat.distrib.PoissonDistribution;

public class OctaveFrontend implements IDistributionChecker {

	private OctaveEngine octave;
	private ReentrantLock dLock, cLock; 

	private boolean newVersion;

	public OctaveFrontend(){
		this.octave = new OctaveEngineFactory().getScriptEngine();
		String[] v = this.octave.getVersion().split("\\.");

		if(Integer.parseInt(v[0]) >= 3 && Integer.parseInt(v[1]) >= 1){
			this.newVersion = true;
		} else{
			this.newVersion = false;
		}
		// disable the echo output
			this.octave.setWriter(null);
		// disable the warnings, specially the warning for ties in the data set.
			 this.octave.setErrorWriter(null);
		this.dLock = new ReentrantLock(true);
		this.cLock = new ReentrantLock(true);
	}


	@Override
	public  IDistribution continuous(double[] values) {
		this.cLock.lock();
		//  putting the values vector to octave dimension [values.length, 1]
		OctaveDouble a = new OctaveDouble(values, values.length, 1);
		octave.put("a", a);
		// calculating the mean of the values. 
		octave.eval("mean = mean(a)");
		double mean = octave.get(OctaveDouble.class, "mean").get(1,1);
		//now getting std
		octave.eval("std = std(a)");
		double std = octave.get(OctaveDouble.class, "std").get(1,1);

		// checking for normal distribution
		octave.eval("[pvaln,ksn,dn] = kolmogorov_smirnov_test_2(a, normrnd(mean, std, size(a,1), 1))");
		double normD = octave.get(OctaveDouble.class, "dn").get(1,1);

		// checking for exponential distribution. 
		// lambda = 1/mean(x)  because of the maximum-likelihood-estimation
		double expD = 1d;
		if(mean > 0){
			octave.eval("[pvalexp,ksexp,dexp] = kolmogorov_smirnov_test_2(a, exprnd( 1 / mean, size(a,1), 1))");
			expD = octave.get(OctaveDouble.class, "dexp").get(1,1);
		}
		octave.eval("min = min(a)");
		double min = octave.get(OctaveDouble.class, "min").get(1,1);
		octave.eval("max = max(a)");
		double max = octave.get(OctaveDouble.class, "max").get(1,1);

		// checking for uniform distribution
		octave.eval("[pvalu,ksu,du] = kolmogorov_smirnov_test_2(a, unifrnd( min, max, size(a,1), 1))");
		double unifD = octave.get(OctaveDouble.class, "du").get(1,1);

		/*
		 * Calculations are done now find the best fitting Distribution.
		 */
		IDistribution distrib;

		if(normD < expD){
			if(normD < unifD){
				// normD is the smallest difference
				distrib = new NormalDistribution(mean, std);
			}else{
				//unifD is the smallest
				distrib = new ContinuousUniformDistribution(min, max);
			}
		}else{
			// expD < normD
			if(expD < unifD){
				// expD is the smallest difference 
				distrib = new ExponentialDistribution(1.0 / mean);
			}else{
				// unifD is the smallest difference
				distrib = new ContinuousUniformDistribution(min, max);	
			}
		}
		// reset the octave instance
		octave.eval("clear");

		this.cLock.unlock();
		return distrib;
	}




	@Override
	public  <T> IDistribution discrete(T[] values, boolean hasMapping) {
		this.dLock.lock();

		double[] occurrences;
		double[] pValues;

		Map<T,Integer> occurrenceMap = new LinkedHashMap<T, Integer>(values.length);
		int num;
		// count how often an attribute occurs
		for(T entry : values){
			if(occurrenceMap.containsKey(entry)){
				num = occurrenceMap.get(entry);
				occurrenceMap.put(entry, Integer.valueOf(num + 1));
			}else{
				occurrenceMap.put(entry, Integer.valueOf(1));
			}
		}
		if(hasMapping){
			occurrences = new double[values.length];
		}else{
			occurrences = toDoubleArray(values);
		}
		pValues = new double[occurrenceMap.size()];
		int attributeType = 0;
		int numFilled = 0;
		int count;

		/*
		 * We want to map the occurrences of the value types and their percentage.
		 * 
		 * Example:  [StringA, StringB, StringA]
		 * leads to:
		 * 		-occurrence: [0,0,1] because of 2 occurrences of type 0 and 1 occurrence of type 1
		 * 		-pValues [2/3 , 1/3] because of the type occurrence ratio 
		 * 
		 * if the input values has no mapping it already contains the number of occurrences and does not 
		 * need to be refilled.
		 */
		double[] binomValues = new double[occurrenceMap.size()];
		for(T attribute : occurrenceMap.keySet()){
			count =  occurrenceMap.get(attribute);
			// calculating the ratio of the attribute occurrence
			pValues[attributeType] = ((double)count) / (double)values.length;
			// only values with mapping needs to fill the occurrence
			if(hasMapping){
				for(int j = 0; j < count; j++){
					// filling the array with the occurrences of a given attribute type
					occurrences[j + numFilled] = attributeType;
				}
				binomValues[attributeType] = attributeType;
				numFilled += count;
			} else{
				// we have Integers here 
				binomValues[attributeType] = ((Integer) attribute).doubleValue();
			}
			attributeType++;
		}

		// now calculate the distribution
		return discrete(occurrences, pValues, binomValues);

	}


	private  IDistribution discrete(double[] occurrences, double[] pValues, double[] binomValues) {

		OctaveDouble a = new OctaveDouble(occurrences, occurrences.length, 1);
		octave.put("a", a);

		octave.eval("mean = mean(a)");
		double meanA = octave.get(OctaveDouble.class, "mean").get(1,1);

		OctaveDouble p = new OctaveDouble(pValues, pValues.length, 1);
		octave.put("pv", p);



		//	octave.eval("meanp = mean(pv)"); // ERROR! ???
		octave.eval("meanp = sum(pv) / size(pv,1)");
		double meanP = octave.get(OctaveDouble.class, "meanp").get(1,1);

		/*
		// assuring the range [0, 1] because if rounding issues
		meanP = Math.max(0.0d, meanP);
		meanP = Math.min(1d, meanP);
		 */
		octave.eval("max = max(a)");
		double max = octave.get(OctaveDouble.class, "max").get(1,1);

		octave.eval("min = min(a)");
		double min = octave.get(OctaveDouble.class, "min").get(1,1);

		String poisFunc;
		if(this.newVersion){
			poisFunc = "poiss";
		} else{
			poisFunc = "poisson";
		}

		// poisson
		double pPois = 0.0d;
		if(meanA < 550){
			try{
				// magical border is 604 (but set to 550 for safety reasons :> ) see: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=418158
				octave.eval("pvalpois = kolmogorov_smirnov_test(a, \"" + poisFunc + "\", round(mean))");
				pPois = octave.get(OctaveDouble.class, "pvalpois").get(1,1);
			} catch(OctaveException e){
				pPois = 0d;
			}
		}
		/*
		octave.eval("pois = poissrnd(mean, size(a,1),1)");
		octave.eval("[pvalpois, kspois, dpois] = kolmogorov_smirnov_test_2(a, pois)");
		double dPois = octave.get(OctaveDouble.class, "dpois").get(1,1);
		 */
		//discrete uniform
		double pUnif;
		String unifFunc;
		if(this.newVersion){
			unifFunc = "unif";
		} else{
			unifFunc = "uniform";
		}

		try{
			octave.eval("pvalunif = kolmogorov_smirnov_test(a, \"" + unifFunc + "\", round(min), round(max))");
			pUnif = octave.get(OctaveDouble.class, "pvalunif").get(1,1);
		}catch(OctaveException e){
			pUnif = 0.0d;
		}
		/*
		double dUnif = 1.0d;
		if(max < 1){
			octave.eval("[pvalunif, ksunif, dunif] = kolmogorov_smirnov_test_2(a, unidrnd(round(max), size(a,1),1))");
			dUnif = octave.get(OctaveDouble.class, "dunif").get(1,1);
		}
		 */
		// binomial distribution
		//	double dBinom = 1.0;
		String binoFunc;
		if(this.newVersion){
			binoFunc = "bino";
		}else{
			binoFunc = "binomial";
		}
		double sumBV = 0;
		double pBino = 0;
		if(pUnif < 0.95d){
			try{
				
				//adding the special binomial values
				OctaveDouble bv = new OctaveDouble(binomValues, binomValues.length, 1);
				octave.put("bv", bv);

				octave.eval("sumBV = sum(bv)");
				sumBV = octave.get(OctaveDouble.class, "sumBV").get(1,1);
				
				while(meanP < 0.1d){
					meanP *= 10;
					sumBV /= 10;
				}
				
				octave.eval("pvalbino = kolmogorov_smirnov_test(a, \"" + binoFunc +"\", " + sumBV + " , " + meanP +")");
				pBino = octave.get(OctaveDouble.class, "pvalbino").get(1,1);
			} catch(OctaveException e){
				pBino = 0d;
			}
		}
		//	try{
		/*
			octave.eval("bino = binornd(round(mean), meanp , size(a,1),1)");
			octave.eval("[pvalbinom, ksbinom, dbinom] = kolmogorov_smirnov_test_2(a, bino)");
			dBinom = octave.get(OctaveDouble.class, "dbinom").get(1,1);
		 */
		/*
		}catch(OctaveEvalException e){
			System.out.println(e.getMessage());
			octave.eval("size = size(a,1)");
			double length = octave.get(OctaveDouble.class, "size").get(1,1);
			System.out.println("ERROR!!!!! meanp: " + meanP + " mean: " + meanA + " size: " + occurrences.length + " = " + length + "?");
		}
		 */
		IDistribution distrib;
		if(pPois > pUnif){
			if(pPois > pBino){
				// dPois < all
				distrib = new PoissonDistribution(meanA);
			}else{
				// dBinom < all
				distrib = new BinomialDistribution(meanP, (int) sumBV);
			}
		}else{
			if(pUnif > pBino){
				// dUnif < all
				distrib = new DiscreteUniformDistribution(min, max);
			}else{
				// dBinom < all
				distrib = new BinomialDistribution(meanP, (int) sumBV);
			}
		}	
		// reset the octave instance
		octave.eval("clear");

		this.dLock.unlock();
		return distrib;
	}

	private  <T> double[]  toDoubleArray(T[] values){
		double[] temp = new double[values.length];
		for(int i = 0; i  < values.length; i++){
			temp[i] = ((Integer) values[i]).doubleValue();
		}
		return temp;
	}


	public void shutdown(){
		this.octave.close();
	}


	/*
	private  double[] pValues(double[] occurrences){
		double sum = 0;
		// first we need to know the amount of samples
		for(int i = 0; i < occurrences.length; i++){
			sum += occurrences[i];
		}
		double[] p = new double[occurrences.length];
		for(int i = 0; i < p.length; i++){
			p[i] = occurrences[i] / sum;
		}

		return p;
	}

	private <T> double[] countOccurences(T[] values){
		Map<T, Integer> occurences = new LinkedHashMap<T, Integer>();
		int num;
		// count the occurrences of the given values
		for(T entry : values){
			if(occurences.containsKey(entry)){
				num = occurences.get(entry);
				occurences.put(entry, Integer.valueOf(num + 1));
			} else{
				occurences.put(entry, Integer.valueOf(1));
			}
		}
		double[] samples = new double[occurences.size()];
		int i = 0;
		for(double entry : occurences.values()){
			samples[i++] = (double) entry;
		}

		return samples;

	}
	 */
}
